有了昨天的 KSP 基礎結構後,今天就讓我們著重在於 Processor module 裡的邏輯!
如果還沒看過上一篇的話,請往這裡去:
https://ithelp.ithome.com.tw/articles/10306377
既然我們把這個 module 叫做 Processor module,那最重要的功能就是 process 這件事情了,KSP 所提供的 SymbolProcessor
就是處理這件事情的物件:
interface SymbolProcessor {
fun process(resolver: Resolver): List<KSAnnotated> //main logic
fun finish() {}
fun onError() {}
}
SymbolProcessor
作為統一的接口,除了 finish
跟 onError
二個不同 terminate 的 callback 之外,最重要的就是 process
這個 function 了,我們會在這個 callback 裡處理大部分的邏輯,而 Resolver
這個參數也是個 interface,它的實作是由系統提供所以我們不用煩惱,而它的 function 也很多所以我們就不一一列出來,總之可以把它想成是一個取得程式碼資訊的中介層。
雖然說 Resolver
有很多 function,但通常我還是會在幫它加上下面的 extension function,比較容易過濾出有我所關注的 annotation 的物件。
fun Resolver.getSymbols(cls: KClass<*>) =
this.getSymbolsWithAnnotation(cls.qualifiedName.orEmpty())
.filterIsInstance<KSClassDeclaration>()
.filter(KSNode::validate)
官網上還列了很多常用的 extension,有興趣的話也可以參考:
https://kotlinlang.org/docs/ksp-examples.html
上面的範例很多 KS 開頭的物件對吧,有 KS 前綴的 class 通常代表著一個程式碼物件,以 KSFile
為首可以展開以下的結構:
KSFile
packageName: KSName
fileName: String
annotations: List<KSAnnotation> (File annotations)
declarations: List<KSDeclaration>
KSClassDeclaration // class, interface, object
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
classKind: ClassKind
primaryConstructor: KSFunctionDeclaration
superTypes: List<KSTypeReference>
// contains inner classes, member functions, properties, etc.
declarations: List<KSDeclaration>
KSFunctionDeclaration // top level function
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
functionKind: FunctionKind
extensionReceiver: KSTypeReference?
returnType: KSTypeReference
parameters: List<KSValueParameter>
// contains local classes, local functions, local variables, etc.
declarations: List<KSDeclaration>
KSPropertyDeclaration // global variable
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
extensionReceiver: KSTypeReference?
type: KSTypeReference
getter: KSPropertyGetter
returnType: KSTypeReference
setter: KSPropertySetter
parameter: KSValueParameter
藉由 Resolver
解析出的 Kotlin Symbol 所蘊含的資訊,我們就可以了解程式碼的結構,進而使用這些資訊建立我們想要自動產生的 kt 檔案,但現在問題來了,這些 SymbolProcessor
是誰會來呼叫呢?
KSP 裡負責建立 SymbolProcessor
的是 SymbolProcessorProvider
,它唯一的任務就是建立這個 processor,以下是這個 SAM 的定義:
fun interface SymbolProcessorProvider {
/**
* Called by Kotlin Symbol Processing to create the processor.
*/
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
SymbolProcessorEnvironment
裡有著各種 parse 時需要的資訊以及物件,原始碼如下:
class SymbolProcessorEnvironment(
/**
* passed from command line, Gradle, etc.
*/
val options: Map<String, String>,
/**
* language version of compilation environment.
*/
val kotlinVersion: KotlinVersion,
/**
* creates managed files.
*/
val codeGenerator: CodeGenerator,
/**
* for logging to build output.
*/
val logger: KSPLogger,
/**
* Kotlin API version of compilation environment.
*/
val apiVersion: KotlinVersion,
/**
* Kotlin compiler version of compilation environment.
*/
val compilerVersion: KotlinVersion,
/**
* Information of target platforms
*
* There can be multiple platforms in a metadata compilation.
*/
val platforms: List<PlatformInfo>,
)
而如果你只需要 codeGenerator
跟 logger
的話,一個具體實作可能長得像這樣:
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return BuilderProcessor(environment.codeGenerator, environment.logger)
}
}
但問題又出現了,系統怎麼知道該建立這個 SymbolProcessorProvider
來得到 SymbolProcessor
呢?
這個問題跟我們寫 android 的時候定義的 Activity
很像,Activity
作為一般的 class 可以在任意地方定義,但要想被系統建立就必須在 AndroidManifest 裡宣告,而這個邏輯不論是 Annotation Processing 或 KSP 也是一樣的,必須要在一個固定的地方宣告,以 KSP 來說就必須定義在 src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
裡:
src
├── java
│ └── ...
└── resources
└── META-INF
└── services
└── com.google.devtools.ksp.processing.SymbolProcessorProvider
而在這個檔案裡呢,只要把一個一個的 SymbolProcessorProvider
,包括 package 一行行列出來就可以了,範例如下:
com.jintin.kfactory.processor.BuilderProcessorProvider
介紹完 Processor 的結構後,讓我們再回頭看看產生檔案這塊,如果你有新增 KotlinPoet 以及 KotlinPoet 的 KSP extension 的話,那寫檔案這塊應該會蠻愉快的,如果沒有的話,可以先加上這些 dependency:
dependencies {
implementation 'com.squareup:kotlinpoet:1.11.0'
implementation 'com.squareup:kotlinpoet-ksp:1.12.0'
}
有了以上設定,我們只要建立 FileSpec
後呼叫 writeTo
就可以了:
fileSpec.writeTo(codeGenerator, Dependencies(true))
而且 KotlinPoet 也提供 Kotlin Symbol 直接轉換成 KotlinPoet 物件的能力,比如 toTypeName
、toClassName
、toKModifier
等都非常實用,尤其當你的物件有很多層的 generic type 的時候,你會非常感謝不用自己手刻遞迴來處理。
KSP 提供了一套標準讓我們可以在 compile 的時候讀取程式碼資訊,並動態加入更多程式碼一起 compile,讓我們可以把重複性的程式碼自動產生,真的是非常棒的工具呢。寫個程式讓程式能自己寫程式真的是非常過癮的一件事情,期待大家也能仔細想想自己的 domain 有哪些應用場景,相信實際寫個一二次就會得心應手了!